package ar.edu.itba.it.pdc.proxy;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;

import org.joda.time.DateTime;

import ar.edu.itba.it.pdc.app.Madonna3;
import ar.edu.itba.it.pdc.app.client.Client;
import ar.edu.itba.it.pdc.app.client.ConnectionManager;
import ar.edu.itba.it.pdc.app.config.mapping.Server;
import ar.edu.itba.it.pdc.protocol.POP3Request;
import ar.edu.itba.it.pdc.protocol.POP3Response;
import ar.edu.itba.it.pdc.protocol.POP3Utils;
import ar.edu.itba.it.pdc.protocol.ValidPOP3Request;
import ar.edu.itba.it.pdc.protocol.ValidPOP3Response;
import ar.edu.itba.it.pdc.protocol.email.Header;
import ar.edu.itba.it.pdc.protocol.email.Mail;
import ar.edu.itba.it.pdc.proxy.stats.StatisticsCollector;
import ar.edu.itba.it.pdc.utils.DeleteConditions;
import ar.edu.itba.it.pdc.utils.FilterUtils;
import ar.edu.itba.it.pdc.utils.LoggingUtils;
import ar.edu.itba.it.pdc.utils.TempFilesSetup;
import ar.edu.itba.it.pdc.utils.UserRestrictions;

public class POP3Proxy {
	
	private static final String CRLF = TempFilesSetup.CRLF;
	
	private static final String WARNING = "WARN - ";
	private static final String W_INTERVALS = WARNING + "At this moment you are not allowed to login as ";
	private static final String W_HITS = WARNING + "Too much logins as ";
	private static final String W_DELETE = WARNING + "A delete condition is preventing you from deleting mail with id ";
	
	private static final String BLOCKED_ADDRESS = "You are in the blacklist\n";
	
	private ServerSocket listener;
	private ConnectionManager connectionManager;
	private StatisticsCollector statisticsCollector = new StatisticsCollector();

	public POP3Proxy(final int port, final int backlog) throws IOException {
		TempFilesSetup.DIR.mkdir();
		this.connectionManager = new ConnectionManager();
		this.listener = new ServerSocket(port, backlog);
		Madonna3.log.info("Madonna3 Proxy Service started in: " + this.listener.getInetAddress().getHostAddress() + ":" + port);
	}

	public void start() {
		do {
			final Client client;
			try {
				Socket s = listener.accept();
				if(isBlocked(s.getInetAddress().getHostAddress())) {
					s.getOutputStream().write(BLOCKED_ADDRESS.getBytes());
					s.getOutputStream().flush();
					s.close();
					continue;
				}
				client = new Client(s);
				Madonna3.log.info("New connection from: " + client.getIP() + ":" + client.getPort());
				if (addConnection(client)) {
					new Thread(new Runnable() {

						public void run() {
							try {
								//pass the server's welcome message
								receiveIn(client, getResponse(client));
								do {
									if (connectionManager.getEndpointFor(client).isClosed())
										client.close();
									else
										sendReceive(client);
								} while (!client.isClosed() || !connectionManager.getEndpointFor(client).isClosed());
							} catch (Exception e) {
								Madonna3.log.error("Problems with client connection: " + e.getMessage());
								removeConnection(client);
							}
						}
						
					}).start();
				}
			} catch (Exception e) {
				Madonna3.log.error("Problems accepting connection");
			}
		} while (true);
	}

	public StatisticsCollector getStatistics() {
		return this.statisticsCollector;
	}

	public boolean addConnection(Client client) {
		Socket serverConnection;
		try {
			try {
				serverConnection = new Socket(Madonna3.getApp().getSettings().getServer().getAddress(), Madonna3.getApp().getSettings().getServer().getPort());
			} catch (UnknownHostException e) {
				Madonna3.log.error("The origin server is down");
				client.close();
				return false;
			}
			connectionManager.addConnection(client, serverConnection);
			return true;
		} catch (IOException e) {
			Madonna3.log.error("IOException:" + e.getMessage());
			try {
				OutputStream out = client.getSocket().getOutputStream();
				out.write("ERR - The server is down\r\n".getBytes());
				out.flush();
				client.getSocket().close();
			} catch (IOException e1) {
				Madonna3.log.error("Can't close client connection");
			}
			return false;
		}
	}

	public void removeConnection(Client client) {
		try {
			connectionManager.getEndpointFor(client).close();
			client.close();
		} catch (Exception e) {
			Madonna3.log.error("Can't close connection");
		}
		connectionManager.removeConnection(client);
	}
	
	private boolean checkHits(Client client, String username) throws IOException {
		int hits = 1 + this.statisticsCollector.getAccessesQuantity(username);
		if (!UserRestrictions.checkHits(username, hits)){
			client.warn(W_HITS + username + "." + CRLF);
			LoggingUtils.logTooMuchLogins(username, client);
			return false;
		}
		return true;
	}
	
	private boolean checkIntervals(Client client, String username) throws IOException {
		if (!UserRestrictions.checkIntervals(username)){
			client.warn(W_INTERVALS + username + CRLF);
			LoggingUtils.logInvalidTimeToLogin(username, client);
			return false;
		}
		return true;
	}
	
	private boolean checkDeleteConditions(Client client, int id) throws IOException {
		if (!DeleteConditions.checkConditions(client, connectionManager.getEndpointFor(client), id)){
			client.warn(W_DELETE + id + "." + CRLF);
			LoggingUtils.logDeleteCondition(client, id);
			return false;
		}
		this.statisticsCollector.oneMoreMailDeletedBy(client.getUsername());
		return true;
	}
	
	private boolean preAnalysis(Client client, POP3Request request) throws IOException {
		// Si la request es inválida, no hay nada que analizar. Pero si la request es válida...
		if (request.isValid()) {			
			ValidPOP3Request validRequest = request.asValid();
			
			if (!client.isLogged()) {
				if (validRequest.is(ValidPOP3Request.Type.USER)) {
					/* CONTROLES DE ACCESO (para clientes no logueados):
					 * 	-	Si un cliente trata de ingresar como un usuario que ya superó su
					 * 	cuota de accesos permitida, entonces se evita ese logueo.
					 * 
					 * 	-	Si un cliente trata de ingresar como un usuario que no tiene permitido
					 * 	el ingreso al sistema en este preciso momento, entonces se evita ese logueo.
					 */
					String possible = validRequest.getArguments().trim();
					if (!checkHits(client, possible)) {
						return false;
					} else if (!checkIntervals(client, possible)) {
						return false;
					}
					client.setUsername(possible);
					
					//Cheakea si el usuario debe mapearse a otro server
					switchServer(client, request);
					
				}
			} else {
				if (validRequest.is(ValidPOP3Request.Type.DELE)) {
					//	CONDICIONES DE BORRADO (sólo tienen sentido con clientes logueados)
					int id = Integer.valueOf(validRequest.getArguments().trim());
					if (!checkDeleteConditions(client, id))
						return false;
					this.statisticsCollector.oneMoreMailDeletedBy(client.getUsername());
					
				}
			}
		}
		return true;
	}
	
	private boolean postAnalysis(Client client, POP3Request request, POP3Response response) throws IOException {

		if (!request.isValid() || !response.isValid())
			return true;
		
		ValidPOP3Request validRequest = request.asValid();
		ValidPOP3Response validResponse = response.asValid();

		if (validResponse.is(ValidPOP3Response.Status.OK)) {
			// El usuario está a punto de salir del sistema.
			if (validRequest.is(ValidPOP3Request.Type.QUIT)) {
				LoggingUtils.logGoodbye(client);
				connectionManager.getEndpointFor(client).close();
				client.close();
				connectionManager.getEndpointFor(client).close();
				return false;
			
			// El usuario se acaba de loguear.
			} else if (validRequest.is(ValidPOP3Request.Type.PASS)) {
				this.statisticsCollector.addAccess(client.getUsername(), new DateTime());
				client.login();
			
			// El usuario está a punto de leer un mail.
			} else if (validRequest.is(ValidPOP3Request.Type.RETR)) {
				this.statisticsCollector.oneMoreMailReadBy(client.getUsername());
				
				if(FilterUtils.thereAreFilters()) {
					Mail mail = Mail.build(new FileInputStream(validResponse.getResponse()));
					mail.addHeader(new Header("X-Filter", " " + FilterUtils.getFiltersName()));
					BufferedReader original = new BufferedReader(new FileReader(response.getResponse()));
					File responseFile = File.createTempFile("transform_", ".madonna3", new File("tmp"));
					FileWriter newResponseFile = new FileWriter(responseFile);
					
					newResponseFile.write(original.readLine() + CRLF);
					newResponseFile.flush();
					for(Header h : mail.getHeaders()) {
						newResponseFile.write(POP3Utils.fold(0, h.getHeader()) + CRLF);
						newResponseFile.flush();
					}
					BufferedReader bodyReader = mail.getBody(); 
					String line;
					while((line = bodyReader.readLine()) != null) {
						newResponseFile.write(line + CRLF);
						newResponseFile.flush();
					}
					response.getResponse().delete();
					response.setResponse(responseFile);
					
				}
			}
		}
		return true;
	}
	
	public void sendReceive(Client client) throws IOException {
		if (client.isLogged() && !checkIntervals(client, client.getUsername()))
			return;

		POP3Request request = getRequest(client);

		// Analiza la request antes de enviarla.
		if (!preAnalysis(client, request))
			return;
			
		sendFrom(client, request);
		
		POP3Response response = getResponse(client);
		
		if (client.isLogged())
			this.statisticsCollector.collectBytes(client.getUsername(), request.getSize(), response.getSize());

		//Analiza la request junto a su response, una vez recibida.
		if (!postAnalysis(client, request, response))
			return;
		
		receiveIn(client, response);

	}

	private POP3Request getRequest(Client from) throws IOException {
		String tmp = POP3Request.createTempString(from.getSocket().getInputStream());
		POP3Request request = POP3Request.build(tmp);
		return request;
	}

	private POP3Response getResponse(Client to) throws IOException {
		POP3Response response = POP3Response.build(connectionManager.getEndpointFor(to).getInputStream());
		return response;
	}

	private void sendFrom(Client from, POP3Request request) throws IOException {
		request.sendTo(connectionManager.getEndpointFor(from));
		LoggingUtils.logSend(from, request);
	}

	private void receiveIn(Client to, POP3Response response) throws IOException {
		response.receiveIn(to);
		LoggingUtils.logReceive(to, response);
	}
	
	private boolean isBlocked(String ip) {
		if(ip.equals("0:0:0:0:0:0:0:1")) {
			if(Madonna3.getApp().getSettings().getProxy().getBlackList() != null) {
				if(Madonna3.getApp().getSettings().getProxy().getBlackList().getAddress().contains("localhost")) {
					return true;
				}
			}
			return false;
		}else{
			if(Madonna3.getApp().getSettings().getProxy().getBlackList() != null) {
				for(String ipOrAddress : Madonna3.getApp().getSettings().getProxy().getBlackList().getAddress()) {
					if(ipOrAddress.contains("/")){
						if(ipInAddress(ip, ipOrAddress)){
							return true;
						}
					}else{
						if(ipOrAddress.contains(ip)) {
							return true;
						}
					}
				}
			}
			return false;
		}
	}
	
	/**
	 * Checkea si una ip pertenece a una red.
	 * @param sip IP.
	 * @param saddress Red.
	 * @return
	 */
	private boolean ipInAddress(String sip, String saddress) {
		try {
			
			System.out.println("Comparando: " + sip + " con " + saddress);
			
			String addressParts[] = saddress.split("/");
			
			int maskN = Integer.valueOf(addressParts[1]);
			System.out.println(addressParts[0]);
			String []snetAdd = addressParts[0].split("\\.");
			String []sipAdd = sip.split("\\.");
			
			String addBin = "";
			String ipBin = "";
			
			for(int i = 0; i < 4; i++) {
				addBin += String.format("%08d", Integer.parseInt(Integer.toBinaryString(Integer.valueOf(snetAdd[i]))));
				ipBin += String.format("%08d", Integer.parseInt(Integer.toBinaryString(Integer.valueOf(sipAdd[i]))));
			}
			
			return addBin.substring(0, maskN).equals(ipBin.substring(0, maskN));
		}catch(Exception e){
			Madonna3.log.error("The are wrong parameters in the settings file");
			return false;
		}
	}
	
	/**
	 * Si en los settings el usuario tiene seteado un servidor diferente del default, cambia el socket
	 * por uno conectado al nuevo servidor.
	 * Para que sea transparente hacia el usuario, se tira el mensaje de bienvenida, y se manda el request
	 * de user al nuevo servidor.
	 * @param client Cliente que contiene el socket conectado al cliente
	 * @param request Request con el pedido "USER ejemplo"
	 */
	private void switchServer(Client client, POP3Request request) {
		try {
			Server aux;
			if((aux = UserRestrictions.changeServer(client.getUsername())) != null) {
				Socket s = connectionManager.getEndpointFor(client);
				s.close(); //Cierro el socket al origin server
				Socket newEndPoint = new Socket(aux.getAddress(), aux.getPort());
				connectionManager.addConnection(client, newEndPoint);

				Client dummy = new Client(new Socket());
				receiveIn(dummy, getResponse(client));
				
				newEndPoint.getOutputStream().write(request.getCommand().getBytes());
				newEndPoint.getOutputStream().write("\n".getBytes());
				newEndPoint.getOutputStream().flush();
				
			}
		}catch(Exception e) {
			Madonna3.log.info("Problems with switchServer: " + e.getMessage());
		}
	}
	
}
